import random

# ============================================================
# OPTIONAL: pip install reedsolo
# ============================================================

from reedsolo import RSCodec

# =========================
# PARAMETERS
# =========================

CARD_THICKNESS = 2

FRAME_HEIGHT = 1
FRAME_WIDTH = 3

HOLE_DIAMETER = 3
HOLE_RADIUS = HOLE_DIAMETER / 2

PITCH = 4

# 16 data bytes + 4 RS bytes
COLS = 20

# 8 bits
ROWS = 8

MARGIN = 4

# ------------------------------------------
# LEFT HANDLE / TAB
# ------------------------------------------

SIDE_TAB_WIDTH = 14
SIDE_TAB_HEIGHT = 3

# Hole in left handle
SIDE_TAB_HOLE_DIAMETER = 6
SIDE_TAB_HOLE_Y_OFFSET = 3

# ------------------------------------------
# STICKER RECESS
# ------------------------------------------

STICKER_RECESS_WIDTH = 12
STICKER_RECESS_HEIGHT = 25
STICKER_RECESS_DEPTH = 1

# Positive values move recess downward
STICKER_RECESS_Y_OFFSET = 5

# =========================
# CARD DIMENSIONS
# =========================

card_w = (
    MARGIN * 2
    + (COLS - 1) * PITCH
    + HOLE_DIAMETER
)

card_h = (
    MARGIN * 2
    + (ROWS - 1) * PITCH
    + HOLE_DIAMETER
)

# =========================
# READ INPUT DATA
# =========================

data_values = []

print("")
print("1 = Enter custom bytes")
print("2 = Generate random bytes")
print("")

mode = input("Selection: ").strip()

# ------------------------------------------
# RANDOM BYTES
# ------------------------------------------

if mode == "2":

    data_values = [
        random.randint(0, 255)
        for _ in range(16)
    ]

    print("")
    print("Random bytes generated:")
    print(data_values)

# ------------------------------------------
# MANUAL INPUT
# ------------------------------------------

else:

    print("")
    print("Enter 16 values between 0 and 255")

    for i in range(16):

        while True:

            try:

                v = int(input(f"Byte {i + 1}: "))

                if 0 <= v <= 255:
                    data_values.append(v)
                    break

                print("Value must be between 0 and 255")

            except:
                print("Invalid value")

# =========================
# REED SOLOMON ENCODING
# =========================

rsc = RSCodec(4)

encoded = list(
    rsc.encode(bytes(data_values))
)

# ------------------------------------------
# Split into data and ECC
# ------------------------------------------

data = encoded[:16]
ecc = encoded[16:]

# ------------------------------------------
# Interleave layout:
#
# D0 D1 D2 D3 RS0
# D4 D5 D6 D7 RS1
# D8 D9 D10 D11 RS2
# D12 D13 D14 D15 RS3
# ------------------------------------------

values = []

for group in range(4):

    start = group * 4

    values.extend(data[start:start + 4])
    values.append(ecc[group])

# =========================
# GENERATE SCAD
# =========================

scad = []

scad.append("$fn=48;")
scad.append("")

scad.append("difference() {")

# ==========================================
# MAIN UNION
# ==========================================

scad.append("    union() {")

# ------------------------------------------
# BASE CARD
# ------------------------------------------

scad.append(
    f"""
    cube([{card_w}, {card_h}, {CARD_THICKNESS}]);
    """
)

# ------------------------------------------
# FRAME
# ------------------------------------------

# top
scad.append(
    f"""
    translate([0,{card_h - FRAME_WIDTH},{CARD_THICKNESS}])
    cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# bottom
scad.append(
    f"""
    translate([0,0,{CARD_THICKNESS}])
    cube([{card_w},{FRAME_WIDTH},{FRAME_HEIGHT}]);
    """
)

# left
scad.append(
    f"""
    translate([0,0,{CARD_THICKNESS}])
    cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# right
scad.append(
    f"""
    translate([{card_w - FRAME_WIDTH},0,{CARD_THICKNESS}])
    cube([{FRAME_WIDTH},{card_h},{FRAME_HEIGHT}]);
    """
)

# ------------------------------------------
# LEFT HANDLE
# ------------------------------------------

scad.append(
    f"""
    translate([-{SIDE_TAB_WIDTH},0,0])
    cube([{SIDE_TAB_WIDTH},{card_h},{SIDE_TAB_HEIGHT}]);
    """
)

# end union
scad.append("    }")

# ==========================================
# CARD DATA HOLES
# ==========================================

for c in range(COLS):

    value = values[c]

    # LSB first
    bits = [
        (value >> i) & 1
        for i in range(8)
    ]

    for r in range(ROWS):

        if bits[r] == 1:

            x = (
                MARGIN
                + HOLE_RADIUS
                + c * PITCH
            )

            y = (
                MARGIN
                + HOLE_RADIUS
                + (ROWS - 1 - r) * PITCH
            )

            scad.append(
                f"""
                translate([{x},{y},-1])
                cylinder(
                    h={CARD_THICKNESS + FRAME_HEIGHT + 10},
                    r={HOLE_RADIUS}
                );
                """
            )

# ==========================================
# HOLE IN LEFT HANDLE
# ==========================================

hole_x = -SIDE_TAB_WIDTH / 2

hole_y = (
    card_h
    - MARGIN
    - SIDE_TAB_HOLE_DIAMETER
    + SIDE_TAB_HOLE_Y_OFFSET
)

scad.append(
    f"""
    translate([{hole_x},{hole_y},-1])
    cylinder(
        h={CARD_THICKNESS + FRAME_HEIGHT + 10},
        r={SIDE_TAB_HOLE_DIAMETER / 2}
    );
    """
)

# ==========================================
# STICKER RECESS
# ==========================================

recess_x = -SIDE_TAB_WIDTH + 2

recess_y = (
    (card_h / 2)
    - (STICKER_RECESS_HEIGHT / 2)
    - STICKER_RECESS_Y_OFFSET
)

scad.append(
    f"""
    translate([
        {recess_x},
        {recess_y},
        {CARD_THICKNESS - STICKER_RECESS_DEPTH}
    ])
    cube([
        {STICKER_RECESS_WIDTH},
        {STICKER_RECESS_HEIGHT},
        {STICKER_RECESS_DEPTH + 1}
    ]);
    """
)

# ==========================================
# END DIFFERENCE
# ==========================================

scad.append("}")

# =========================
# SAVE FILE
# =========================

filename = "punchcard.scad"

with open(filename, "w", encoding="utf-8") as f:
    f.write("\n".join(scad))

# =========================
# DISPLAY BYTE INFO
# =========================

print("")
print("Card byte layout:")
print("")

for i, value in enumerate(values):

    bits = [
        (value >> b) & 1
        for b in range(8)
    ]

    # every 5th byte is ECC
    if (i + 1) % 5 == 0:
        label = f"RS  {(i + 1)//5}"
    else:
        data_index = i - (i // 5)
        label = f"DAT {data_index+1:02d}"

    print(
        f"{label}: "
        f"decimal={value:3d} "
        f"bits(LSB->MSB)="
        f"{''.join(map(str, bits))}"
    )

print("")
print(f"SCAD file created: {filename}")
print("")
print("Open the file in OpenSCAD")
print("Press F6")
print("Then export as STL")